﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MemoryDiagnosticsHelper.cs" company="XamlNinja">
//   2010
// </copyright>
// <summary>
//   Helper class for showing current memory usage
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace BingMaps.MemoryDiagnostics
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Controls.Primitives;
    using System.Windows.Media;
    using System.Windows.Threading;

    using Microsoft.Phone.Info;

    /// <summary>
    /// Helper class for showing current memory usage
    /// </summary>
    public static class MemoryDiagnosticsHelper
    {
        #region Constants and Fields

        /// <summary>
        /// The ma x_ checkpoints.
        /// </summary>
        private const long MAX_CHECKPOINTS = 10; // adjust as needed

        /// <summary>
        /// The ma x_ memory.
        /// </summary>
        private const long MAX_MEMORY = 90 * 1024 * 1024; // 90MB, per marketplace

        /// <summary>
        /// The already failed peak.
        /// </summary>
        private static bool alreadyFailedPeak; // to avoid endless Asserts

        /// <summary>
        /// The current memory block.
        /// </summary>
        private static TextBlock currentMemoryBlock;

        /// <summary>
        /// The force gc.
        /// </summary>
        private static bool forceGc;

        /// <summary>
        /// The last safety band.
        /// </summary>
        private static int lastSafetyBand = -1; // to avoid needless changes of colour

        /// <summary>
        /// The peak memory block.
        /// </summary>
        private static TextBlock peakMemoryBlock;

        /// <summary>
        /// The popup.
        /// </summary>
        private static Popup popup;

        /// <summary>
        /// The recent checkpoints.
        /// </summary>
        private static Queue<MemoryCheckpoint> recentCheckpoints;

        /// <summary>
        /// The timer.
        /// </summary>
        private static DispatcherTimer timer;

        #endregion

        #region Properties

        /// <summary>
        /// Recent checkpoints stored by the app; will always be empty in retail mode
        /// </summary>
        public static IEnumerable<MemoryCheckpoint> RecentCheckpoints
        {
            get
            {
                if (recentCheckpoints == null)
                {
                    yield break;
                }

                foreach (MemoryCheckpoint checkpoint in recentCheckpoints)
                {
                    yield return checkpoint;
                }
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Add a checkpoint to the system to help diagnose failures. Ignored in retail mode
        /// </summary>
        /// <param name="text">
        /// Text to describe the most recent thing that happened
        /// </param>
        public static void Checkpoint(string text)
        {
            if (recentCheckpoints == null)
            {
                return;
            }

            if (recentCheckpoints.Count >= MAX_CHECKPOINTS - 1)
            {
                recentCheckpoints.Dequeue();
            }

            recentCheckpoints.Enqueue(new MemoryCheckpoint(text, GetCurrentMemoryUsage()));
        }

        /// <summary>
        /// Gets the current memory usage, in bytes. Returns zero in non-debug mode
        /// </summary>
        /// <returns>
        /// Current usage
        /// </returns>
        public static long GetCurrentMemoryUsage()
        {
            // #if DEBUG

            // don't use DeviceExtendedProperties for release builds (requires a capability)
            return (long)DeviceExtendedProperties.GetValue("ApplicationCurrentMemoryUsage");

            // #else
            return 0;

            // #endif
        }

        /// <summary>
        /// Gets the peak memory usage, in bytes. Returns zero in non-debug mode
        /// </summary>
        /// <returns>
        /// Peak memory usage
        /// </returns>
        public static long GetPeakMemoryUsage()
        {
            // #if DEBUG

            // don't use DeviceExtendedProperties for release builds (requires a capability)
            return (long)DeviceExtendedProperties.GetValue("ApplicationPeakMemoryUsage");

            // #else
            return 0;

            // #endif
        }

        /// <summary>
        /// Starts the memory diagnostic timer and shows the counter
        /// </summary>
        /// <param name="timespan">
        /// The timespan between counter updates
        /// </param>
        /// <param name="forceGc">
        /// Whether or not to force a GC before collecting memory stats
        /// </param>
        public static void Start(TimeSpan timespan, bool forceGc)
        {
            if (timer != null)
            {
                throw new InvalidOperationException("Diagnostics already running");
            }

            MemoryDiagnosticsHelper.forceGc = forceGc;
            recentCheckpoints = new Queue<MemoryCheckpoint>();

            StartTimer(timespan);
            ShowPopup();
        }

        /// <summary>
        /// Stops the timer and hides the counter
        /// </summary>
        public static void Stop()
        {
            HidePopup();
            StopTimer();
            recentCheckpoints = null;
        }

        #endregion

        #region Methods

        /// <summary>
        /// The get brush for safety band.
        /// </summary>
        /// <param name="safetyBand">
        /// The safety band.
        /// </param>
        /// <returns>
        /// </returns>
        private static Brush GetBrushForSafetyBand(int safetyBand)
        {
            switch (safetyBand)
            {
                case 0:
                    return new SolidColorBrush(Colors.Green);

                case 1:
                    return new SolidColorBrush(Colors.Orange);

                default:
                    return new SolidColorBrush(Colors.Red);
            }
        }

        /// <summary>
        /// The get safety band.
        /// </summary>
        /// <param name="mem">
        /// The mem.
        /// </param>
        /// <returns>
        /// The get safety band.
        /// </returns>
        private static int GetSafetyBand(long mem)
        {
            double percent = mem / (double)MAX_MEMORY;
            if (percent <= 0.75)
            {
                return 0;
            }

            if (percent <= 0.90)
            {
                return 1;
            }

            return 2;
        }

        /// <summary>
        /// The hide popup.
        /// </summary>
        private static void HidePopup()
        {
            popup.IsOpen = false;
            popup = null;
        }

        /// <summary>
        /// The show popup.
        /// </summary>
        private static void ShowPopup()
        {
            popup = new Popup();
            double fontSize = (double)Application.Current.Resources["PhoneFontSizeSmall"] - 2;
            var foreground = (Brush)Application.Current.Resources["PhoneForegroundBrush"];
            var sp = new StackPanel
                {
                    Orientation = Orientation.Horizontal, 
                    Background = (Brush)Application.Current.Resources["PhoneSemitransparentBrush"]
                };
            currentMemoryBlock = new TextBlock { Text = "---", FontSize = fontSize, Foreground = foreground };
            peakMemoryBlock = new TextBlock
                {
                    Text = string.Empty, 
                    FontSize = fontSize, 
                    Foreground = foreground, 
                    Margin = new Thickness(5, 0, 0, 0)
                };
            sp.Children.Add(currentMemoryBlock);
            sp.Children.Add(new TextBlock { Text = " kb", FontSize = fontSize, Foreground = foreground });
            sp.Children.Add(peakMemoryBlock);
            sp.RenderTransform = new CompositeTransform
                {
                   Rotation = 90, TranslateX = 480, TranslateY = 425, CenterX = 0, CenterY = 0 
                };
            popup.Child = sp;
            popup.IsOpen = true;
        }

        /// <summary>
        /// The start timer.
        /// </summary>
        /// <param name="timespan">
        /// The timespan.
        /// </param>
        private static void StartTimer(TimeSpan timespan)
        {
            timer = new DispatcherTimer();
            timer.Interval = timespan;
            timer.Tick += timer_Tick;
            timer.Start();
        }

        /// <summary>
        /// The stop timer.
        /// </summary>
        private static void StopTimer()
        {
            timer.Stop();
            timer = null;
        }

        /// <summary>
        /// The update current memory usage.
        /// </summary>
        private static void UpdateCurrentMemoryUsage()
        {
            long mem = GetCurrentMemoryUsage();
            currentMemoryBlock.Text = string.Format("{0:N}", mem / 1024);
            int safetyBand = GetSafetyBand(mem);
            if (safetyBand != lastSafetyBand)
            {
                currentMemoryBlock.Foreground = GetBrushForSafetyBand(safetyBand);
                lastSafetyBand = safetyBand;
            }
        }

        /// <summary>
        /// The update peak memory usage.
        /// </summary>
        private static void UpdatePeakMemoryUsage()
        {
            if (alreadyFailedPeak)
            {
                return;
            }

            long peak = GetPeakMemoryUsage();
            if (peak >= MAX_MEMORY)
            {
                alreadyFailedPeak = true;
                Checkpoint("*MEMORY USAGE FAIL*");
                peakMemoryBlock.Text = "FAIL!";
                peakMemoryBlock.Foreground = new SolidColorBrush(Colors.Red);
                if (Debugger.IsAttached)
                {
                    Debug.Assert(false, "Peak memory condition violated");
                }
            }
        }

        /// <summary>
        /// The timer_ tick.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private static void timer_Tick(object sender, EventArgs e)
        {
            if (forceGc)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }

            UpdateCurrentMemoryUsage();
            UpdatePeakMemoryUsage();
        }

        #endregion
    }

    /// <summary>
    /// Holds checkpoint information for diagnosing memory usage
    /// </summary>
    public class MemoryCheckpoint
    {
        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="MemoryCheckpoint"/> class. 
        /// Creates a new instance
        /// </summary>
        /// <param name="text">
        /// Text for the checkpoint
        /// </param>
        /// <param name="memoryUsage">
        /// Memory usage at the time of the checkpoint
        /// </param>
        internal MemoryCheckpoint(string text, long memoryUsage)
        {
            this.Text = text;
            this.MemoryUsage = memoryUsage;
        }

        #endregion

        #region Properties

        /// <summary>
        /// The memory usage at the time of the checkpoint
        /// </summary>
        public long MemoryUsage { get; private set; }

        /// <summary>
        /// The text associated with this checkpoint
        /// </summary>
        public string Text { get; private set; }

        #endregion
    }
}